/*
 *  Copright: 2008 Philipp Leitner, Distributed Systems Group,
 *            TU Wien, leitner@infosys.tuwien.ac.at
 *
 *  This file is part of DAIOS.
 *
 *  DAIOS is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as
 *  published by the Free Software Foundation.
 * 
 *  DAIOS is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with DAIOS.  If not, see
 *  <http://www.gnu.org/licenses/>.
 * 
 */

package at.ac.tuwien.infosys.dsg.daiosPlugins.axis2;

import java.io.IOException;
import java.net.URL;
import java.util.logging.Logger;

import javax.wsdl.WSDLException;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMElement;
import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.client.Options;
import org.apache.axis2.client.ServiceClient;
import org.apache.axis2.client.async.AsyncResult;
import org.apache.axis2.client.async.Callback;
import org.apache.axis2.transport.http.HTTPConstants;

import at.ac.tuwien.infosys.dsg.daios.exceptions.AmbigiousException;
import at.ac.tuwien.infosys.dsg.daios.exceptions.InvocationException;
import at.ac.tuwien.infosys.dsg.daios.exceptions.NoEndpointException;
import at.ac.tuwien.infosys.dsg.daios.exceptions.UnsupportedMessageTypeException;
import at.ac.tuwien.infosys.dsg.daios.framework.DaiosInterceptor;
import at.ac.tuwien.infosys.dsg.daios.framework.PollObject;
import at.ac.tuwien.infosys.dsg.daios.framework.interfaces.IConstants;
import at.ac.tuwien.infosys.dsg.daios.framework.interfaces.IDaiosCallback;
import at.ac.tuwien.infosys.dsg.daios.framework.interfaces.IServiceFrontendImplementor;
import at.ac.tuwien.infosys.dsg.daios.framework.structs.DaiosInputMessage;
import at.ac.tuwien.infosys.dsg.daios.framework.structs.DaiosOutputMessage;
import at.ac.tuwien.infosys.dsg.daios.util.RESTURLEncoder;
import at.ac.tuwien.infosys.dsg.daios.util.Utility;
import at.ac.tuwien.infosys.dsg.daiosPlugins.axis2.axiom.OMElementRESTConverter;
import at.ac.tuwien.infosys.dsg.daiosPlugins.axis2.axiom.OMUtil;

/**
 * The service invoker used to call a RESTful Web Service using the
 * Axis2 Web Service client libraries.
 * 
 * @author philipp
 *
 */
public class Axis2RESTInvoker implements IServiceFrontendImplementor {
	
	private DaiosInterceptor interceptor = null;
	private Logger log = null;
	private boolean postRequest = true;
	private URL endpoint = null;
	private String example = null;
	private Options axis2Options = null;
	private OMElementRESTConverter converter = null;
	
	/**
	 * Construct a new invoker that calls the service,
	 * with an example being available at a given
	 * URL (REST by example). If no example is given a
	 * HTTP GET invocation is assumed.
	 * 
	 * @param url The location of the service definition, or
	 * null in case of a HTTP GET invocation.
	 * 
	 * @throws WSDLException
	 * @throws AmbigiousException 
	 * @throws IOException
	 */
	Axis2RESTInvoker(URL url, DaiosInterceptor interceptor)
		throws WSDLException, AmbigiousException, IOException {
		
		this.interceptor = interceptor;
		
		log = Logger.getLogger(IConstants.LOG_NAME);
		
		converter = OMElementRESTConverter.getInstance();
		
		// if we did not receive a URL we assume a GET request
		if(url == null)
			postRequest = false;
		
		// load example
		if(postRequest) {
			if(interceptor != null)
				interceptor.beginRESTExampleFetching(url, this);
			example = Utility.getDocumentFromURL(url);
			if(interceptor != null)
				interceptor.finishedRESTExampleFetching(url, this, example);			
		}
		
	}
	
	/**
	 * Do a synchronous, blocking, request-response style
	 * call. The client will be blocked until the return
	 * message is delivered.
	 * 
	 * @param message The Daios style input message.
	 * @return The Daios style output message.
	 * 
	 * @throws AmbigiousException
	 * @throws InvocationException
	 */		
	public DaiosOutputMessage requestResponse(DaiosInputMessage message)
		throws AmbigiousException, InvocationException {
		
		log.info("Doing RESTful requestResponse call.");
		
		if(interceptor != null)
			interceptor.beginInvocation("REST", "requestResponse",
				message, this);
		
		try {
		
			// check if we have an endpoint set (we need it for REST calls!)
			if(endpoint == null)
				throw new NoEndpointException("No endpoint set for REST invocation.");
			
			// prepare RESTful call (i.e. do all the matching and guessing)
			OMElement axisOp = null;
			
			if(postRequest) {
				log.info("Using HTTP POST for REST call.");
				axisOp = prepareAxis2RestPOSTCall(message);
			}
			else {
				log.info("Using HTTP GET for REST call.");
				axisOp = prepareAxis2RestGETCall(message);
				log.info("Using GET endpoint "+axis2Options.getTo());
			}
			
			// do blocking REST invocation
			OMElement result = null;
			ServiceClient sender = new ServiceClient();
			sender.setOptions(axis2Options);
			if(interceptor != null)
				// TODO: serializing the OM model here slows the app !!!
				interceptor.doHTTPInvocation(endpoint.toExternalForm(),
					axisOp.toString(), this);
			result = sender.sendReceive(axisOp);
			if(interceptor != null)
				// TODO: serializing the OM model here slows the app !!!
				interceptor.receiveHTTPResult(endpoint.toExternalForm(),
						axisOp.toString(), this, result.toString());
	        
			// wrap result back into Message
			if(interceptor != null)
				// TODO: serializing the OM model here slows the app !!!
				interceptor.beginOutputConversion(message, null, null, this,
						result.toString());
			DaiosOutputMessage returnMsg = 
				converter.convert(result);
			if(interceptor != null)
				// TODO: serializing the OM model here slows the app !!!
				interceptor.finishedOutputConversion(message, null, null, this,
						result.toString(), returnMsg);	        
			log.info("Message:\n"+returnMsg.toString());

			if(interceptor != null)
				interceptor.finishedInvocation("REST", "requestResponse",
					message, this, returnMsg);
			
			// return 
			return returnMsg;
			
		} catch(NoEndpointException e) {
			throwInvocationException(e);
			return null;
		} catch (UnsupportedMessageTypeException e) {
			throwInvocationException(e);
			return null;
		} catch (AxisFault e) {
			throwInvocationException(e);
			return null;
		}
		
	}

	/**
	 * 
	 * Do an asynchronous 'fire and forget'-style call.
	 * The client fires the input message, and does not
	 * expect any kind of answer or confirmation from the
	 * service.
	 * 
	 * @param message The Daios style input message.
	 * 
	 * @throws AmbigiousException
	 * @throws InvocationException
	 */		
	public void fireAndForget(DaiosInputMessage message)
		throws AmbigiousException, InvocationException {
		
		log.info("Doing RESTful fireAndForget call.");
		
		if(interceptor != null)
			interceptor.beginInvocation("REST", "fireandforget",
				message, this);
		
		try {
		
			// check if we have an endpoint set (we need it for REST calls!)
			if(endpoint == null)
				throw new NoEndpointException("No endpoint set for REST invocation.");
			
			// prepare RESTful call (i.e. do all the matching and guessing)
			OMElement axisOp = null;
			
			if(postRequest) {
				log.info("Using HTTP POST for REST call.");
				axisOp = prepareAxis2RestPOSTCall(message);
			}
			else {
				log.info("Using HTTP GET for REST call.");
				axisOp = prepareAxis2RestGETCall(message);
				log.info("Using GET endpoint "+axis2Options.getTo());
			}
			
			// do fireAndForget REST invocation
			ServiceClient sender = new ServiceClient();
			sender.setOptions(axis2Options);
			if(interceptor != null)
				// TODO: serializing the OM model here slows the app !!!
				interceptor.doHTTPInvocation(endpoint.toExternalForm(),
						axisOp.toString(), this);
			sender.fireAndForget(axisOp);
			
			if(interceptor != null)
				interceptor.finishedInvocation("REST", "fireandforget",
					message, this, null);
			
		} catch(NoEndpointException e) {
			throwInvocationException(e);
		} catch (UnsupportedMessageTypeException e) {
			throwInvocationException(e);
		} catch (AxisFault e) {
			throwInvocationException(e);
		}
			
	}

	/**
	 * Do an asynchronous 'callback'-style call.
	 * The client fires the input message, and registers
	 * a <i>callback listener</i> which is notified
	 * when the response is available. The listener
	 * has to implement the interface
	 * <link>at.ac.tuwien.infosys.dsg.daios.framework.interfaces.DaiosCallback</link>
	 * and implement the <i>onComplete</i> and <i>onError</i>
	 * methods accordingly.
	 * 
	 * @param message The Daios-style input message.
	 * @param callback The registered callback listener.
	 * 
	 * @throws AmbigiousException
	 * @throws InvocationException
	 */	
	public void callback(DaiosInputMessage message, IDaiosCallback callback)
		throws AmbigiousException, InvocationException {
		
		log.info("Doing RESTful callback call.");
		
		if(interceptor != null)
			interceptor.beginInvocation("REST", "callback",
				message, this);
		
		try {
		
			// check if we have an endpoint set (we need it for REST calls!)
			if(endpoint == null)
				throw new NoEndpointException("No endpoint set for REST invocation.");
			
			// prepare RESTful call (i.e. do all the matching and guessing)
			OMElement axisOp = null;
			
			if(postRequest) {
				log.info("Using HTTP POST for REST call.");
				axisOp = prepareAxis2RestPOSTCall(message);
			}
			else {
				log.info("Using HTTP GET for REST call.");
				axisOp = prepareAxis2RestGETCall(message);
				log.info("Using GET endpoint "+axis2Options.getTo());
			}
			
			// create callback wrapper
			DaiosRESTCallbackHandler wrapper =
				new DaiosRESTCallbackHandler(callback, interceptor);
			
			// do non-blocking REST callback invocation
			ServiceClient sender = new ServiceClient();
			sender.setOptions(axis2Options);
			if(interceptor != null)
				// TODO: serializing the OM model here slows the app !!!
				interceptor.doHTTPInvocation(endpoint.toExternalForm(),
					axisOp.toString(), this);
			sender.sendReceiveNonBlocking(axisOp,wrapper);
			
		} catch(NoEndpointException e) {
			throwInvocationException(e);
		} catch (UnsupportedMessageTypeException e) {
			throwInvocationException(e);
		} catch (AxisFault e) {
			throwInvocationException(e);
		}
			
	}

	/**
	 * Do an asynchronous 'poll object'-style call. The client
	 * fires the input message, and receives a 'poll object'
	 * as response. This poll object can be 'queried' for the
	 * result of the invocation as soon as it is available.
	 * 
	 * <p>
	 * 
	 * The poll object call is an abstraction of the more
	 * general callback call.
	 * 
	 * @param message The Daios-style input message.
	 * @return The poll object which will contain the result
	 * as soon as it is available.
	 * 
	 * @throws AmbigiousException
	 * @throws InvocationException
	 */		
	public PollObject pollObjectCall(DaiosInputMessage message)
		throws AmbigiousException, InvocationException {
		
		log.info("Doing REST Poll Object call.");
		
		if(interceptor != null)
			interceptor.beginInvocation("REST", "pollobject",
				message, this);
		
		try {
			
			// check if we have an endpoint set (we need it for REST calls!)
			if(endpoint == null)
				throw new NoEndpointException("No endpoint set for REST invocation.");
			
			// prepare RESTful call (i.e. do all the matching and guessing)
			OMElement axisOp = null;
			
			if(postRequest) {
				log.info("Using HTTP POST for REST call.");
				axisOp = prepareAxis2RestPOSTCall(message);
			}
			else {
				log.info("Using HTTP GET for REST call.");
				axisOp = prepareAxis2RestGETCall(message);
				log.info("Using GET endpoint "+axis2Options.getTo());
			}
	
			// create a poll object for the client
			Axis2PollObject po = new Axis2PollObject();
			
		    // create callback handler for Axis2
			Callback callback = new DaiosRESTPOCallbackHandler(po, interceptor);
			
			// do non-blocking invocation
			ServiceClient sender = new ServiceClient();
			sender.setOptions(axis2Options);
			if(interceptor != null)
				// TODO: serializing the OM model here slows the app !!!
				interceptor.doHTTPInvocation(endpoint.toExternalForm(),
					axisOp.toString(), this);
			sender.sendReceiveNonBlocking(axisOp,callback);
			
			// return poll object
			return po;
			
		} catch(NoEndpointException e) {
			throwInvocationException(e);
			return null;
		} catch (UnsupportedMessageTypeException e) {
			throwInvocationException(e);
			return null;
		} catch (AxisFault e) {
			throwInvocationException(e);
			return null;
		}
			
	}

	/**
	 * Set the endpoint address of the service. Note that
	 * it is mandatory to use this method for RESTful invocations.
	 * 
	 * @param endpoint The address of the endpoint.
	 */		
	public void setEndpointAddress(URL endpoint) {
		
		this.endpoint = endpoint;
		
	}

	/**
	 * Not supported by REST. 
	 */
	public void setOperation(QName operation) throws AmbigiousException {
		
		throw new UnsupportedOperationException
			("setOperation  not supported in REST invocation.");
		
	}

	/**
	 * Not supported by REST. 
	 */
	public void setInputMessage(QName message) throws AmbigiousException {
		
		throw new UnsupportedOperationException
			("setInputMessage  not supported in REST invocation.");
		
	}
	
	/**
	 * Set a list of specific Axis2 options to use. These
	 * options will be used in addition to the default options
	 * for the following invocation(s).
	 * 
	 * @param options The Axis2 options to set.
	 */
	public void setAxis2Options(Options options) {
		this.axis2Options = options;
	}

	/**
	 * Gets the currently set Axis2 options.
	 * 
	 * @return options The current Axis2 options.
	 */
	public Options getAxis2Options() {
		return axis2Options;
	}
	
	/**
	 * Set HTTP method to use (HTTP GET or HTTP POST).
	 * 
	 * @param method
	 */
	public void setHTTPMethod(String method) {
		
		if(method.equalsIgnoreCase(IConstants.HTTP_GET_METHOD))
			postRequest = false;
		else {
			if(method.equalsIgnoreCase(IConstants.HTTP_GET_METHOD))
				postRequest = true;
			else
				throw new IllegalArgumentException("HTTP method "+method+" is "+
					"not supported.");
		}
	}
	
	/**
	 * Hook an interceptor into this frontend.
	 * 
	 * @param interceptor
	 */	
	public void setInterceptor(DaiosInterceptor interceptor) {
		this.interceptor = interceptor;
	}
	
	/**
	 * Prepare a HTTP POST invocation. Generate the default Axis2 options
	 * for REST invocations, convert the input
	 * message given to XML (according to a given example,
	 * REST by example).
	 * 
	 * @param message The input message to use.
	 * @throws UnsupportedMessageTypeException
	 */		
	private OMElement prepareAxis2RestPOSTCall(DaiosInputMessage message)
		throws AmbigiousException {
		
		// generate options for REST call
		if(axis2Options == null)
			axis2Options = new Options();
		axis2Options.setTo(new EndpointReference(endpoint.toExternalForm()));
		axis2Options.setProperty(Constants.Configuration.ENABLE_REST, Constants.VALUE_TRUE);
		axis2Options.setProperty(Constants.Configuration.CONTENT_TYPE,
        		HTTPConstants.MEDIA_TYPE_TEXT_XML);
		
		if(interceptor != null)
			interceptor.beginInputConversion(message, null,
					null, null, this);		
		
		// convert the input message according to the example XML
		try {
			OMElement bodyAsOM =
				converter.convert(message, OMUtil.getRootNode(example));
			if(interceptor != null)
				// TODO: serializing the OM model here slows the app !!!
				interceptor.finishedInputConversion(message, null,
						null, null, this, bodyAsOM.toString());	
			return bodyAsOM;
		} catch (XMLStreamException e) {
			log.severe("There was an error while processing your "+
				"REST example file.");
			e.printStackTrace();
			return null;
		}
		
	}
	
	/**
	 * Prepare a HTTP GET invocation. The most important step
	 * carried out here is the conversion of the input message
	 * to URL parameters (i.e. the endpoint is modified with
	 * request parameters '?param1=value1&param2=value2'). Additionally
	 * the necessary Axis2 options for REST GET invocations are 
	 * generated.
	 * 
	 * @param message The input message to use.
	 * @throws UnsupportedMessageTypeException
	 */	
	private OMElement prepareAxis2RestGETCall(DaiosInputMessage message)
		throws UnsupportedMessageTypeException {
		
		// convert message to URL representation
		URL endpointWithParams = 
			RESTURLEncoder.encodeToURL(endpoint, message);
		
		// generate options for REST GET call
		if(axis2Options == null)
			axis2Options = new Options();
		axis2Options.setTo(new EndpointReference(endpointWithParams.toExternalForm()));
		axis2Options.setProperty(Constants.Configuration.ENABLE_REST, Constants.VALUE_TRUE);
		axis2Options.setProperty (Constants.Configuration.HTTP_METHOD,
        		Constants.Configuration.HTTP_METHOD_GET);
		axis2Options.setProperty(Constants.Configuration.CONTENT_TYPE,
        		HTTPConstants.MEDIA_TYPE_TEXT_XML);
		
		// return some random dummy element
		return OMAbstractFactory.getOMFactory().createOMElement("dummy",null);
		
	}	
	
	/**
	 * An error occured. Throw an InvocationException.
	 * 
	 * @param e
	 * @throws InvocationException
	 */
	private void throwInvocationException(Exception e) throws InvocationException {
		
		log.severe("Exception thrown during Daios processing:");
		e.printStackTrace();
		throw new InvocationException(e);
		
	}
	
	/**
	 * Convert an asynchronously received document into a Daios message.
	 * For some reason we sometimes receive different messages from
	 * Axis2 depending on whether we use synchronous or asynchronous
	 * invocations. This might be a bug in Axis2.
	 * 
	 * @param result The asynchronous result to convert.
	 * @return The converted Daios message.
	 */
	private DaiosOutputMessage convertAsyncRESTResponse(OMElement result) {
		
		DaiosOutputMessage returnMsg = null;
		returnMsg = 
			OMElementRESTConverter.getInstance()
				.convertAsync(result);
		return returnMsg;
		
	}
	
	/**
	 * A callback handler for Axis2 used for Daios callback
	 * invocations (PollObject invocations are using a
	 * different callback handler).
	 * 
	 * @author philipp
	 *
	 */
	class DaiosRESTCallbackHandler extends Callback {
		
		private IDaiosCallback callback = null;
		private DaiosInterceptor interceptor = null;
		
		/**
		 * Protected constructor. Create a new callback handler
		 * wrapped around a IDaiosCallback.
		 * 
		 * @param callback The Daios callback handler. We will
		 * just forward every message we receive in this Axis2
		 * handler to the Daios handler.
		 * @param interceptor 
		 */
		DaiosRESTCallbackHandler(IDaiosCallback callback, DaiosInterceptor interceptor) {
			this.callback = callback;
			this.interceptor = interceptor;
		}
		
		/**
		 * This is the callback method that will get notified
		 * when the invocation ended <i>successfully</i>. In
		 * that case the handler will notify the underlying
		 * Daios handler.
		 * 
		 * @param result The result of the successful
		 * invocation.
		 */
		public void onComplete(AsyncResult result) {
		
			if(interceptor != null)
				// TODO: serializing the OM model here slows the app !!!
				interceptor.receiveHTTPResult(endpoint.toExternalForm(),
					null, Axis2RESTInvoker.this,
					result.getResponseEnvelope().toString());
			
			if(interceptor != null)
				// TODO: serializing the OM model here slows the app !!!
				interceptor.beginOutputConversion(null, null, null, Axis2RESTInvoker.this,
						result.getResponseEnvelope().toString());
			
			// convert result back
			DaiosOutputMessage returnMsg = 
				convertAsyncRESTResponse(result.getResponseEnvelope());

			if(interceptor != null)
				// TODO: serializing the OM model here slows the app !!!
				interceptor.finishedOutputConversion(null, null, null, Axis2RESTInvoker.this,
						result.getResponseEnvelope().toString(), returnMsg);			
			
			if(interceptor != null)
				interceptor.finishedInvocation("REST", "callback",
					null, Axis2RESTInvoker.this, returnMsg);
			
			callback.onComplete(returnMsg);
			
		}

		/**
		 * This is the callback method that will get notified
		 * when the invocation ended <i>unsuccessfully</i>, i.e.
		 * with an error. The handler will notify the wrapped callback
		 * handler about the error. 
		 * 
		 * @param e The exception that has been thrown in the course
		 * of the invocation.
		 */
		public void onError(Exception e) {
			
			log.info("There was an error while executing:\n"+e.getMessage());
			
			callback.onError(e);
			
		}

	}
	
	/**
	 * A callback handler for Axis2 used for Daios Poll Object
	 * invocations.
	 * 
	 * @author philipp
	 *
	 */
	class DaiosRESTPOCallbackHandler extends Callback {
		
		private Axis2PollObject po = null;
		private DaiosInterceptor interceptor = null;
		
		/**
		 * Protected constructor. Create a new callback handler
		 * wrapped around a PollObject.
		 * 
		 * @param po The Poll Object that is going to be notified
		 * when the callback handler receives a result.
		 * @param interceptor 
		 */
		DaiosRESTPOCallbackHandler(Axis2PollObject po, DaiosInterceptor interceptor) {
			this.po = po;
			this.interceptor = interceptor;
		}
		
		/**
		 * This is the callback method that will get notified
		 * when the invocation ended <i>successfully</i>. In
		 * that case the handler will notify the wrapped Poll
		 * Object.
		 * 
		 * @param result The result of the successful
		 * invocation.
		 */
		public void onComplete(AsyncResult result) {
			
			if(interceptor != null)
				// TODO: serializing the OM model here slows the app !!!
				interceptor.receiveHTTPResult(endpoint.toExternalForm(),
					null, Axis2RESTInvoker.this,
					result.getResponseEnvelope().toString());
			
			if(interceptor != null)
				// TODO: serializing the OM model here slows the app !!!
				interceptor.beginOutputConversion(null, null, null, Axis2RESTInvoker.this,
						result.getResponseEnvelope().toString());
			
			// convert result back
			DaiosOutputMessage returnMsg = 
				convertAsyncRESTResponse(result.getResponseEnvelope());
			
			if(interceptor != null)
				// TODO: serializing the OM model here slows the app !!!
				interceptor.finishedOutputConversion(null, null, null, Axis2RESTInvoker.this,
						result.getResponseEnvelope().toString(), returnMsg);
			
			if(interceptor != null)
				interceptor.finishedInvocation("REST", "pollobject",
					null, Axis2RESTInvoker.this, returnMsg);
			
			po.setResult(returnMsg);
			
		}

		/**
		 * This is the callback method that will get notified
		 * when the invocation ended <i>unsuccessfully</i>, i.e.
		 * with an error. The handler will notify the wrapped Poll
		 * object about the error. 
		 * 
		 * @param e The exception that has been thrown in the course
		 * of the invocation.
		 */			
		public void onError(Exception e) {
			
			log.severe("There was an error while executing:\n"+e.getMessage());
			
			po.setError(e);
			
		}

	}
	
}